// Package auth provides an API to use GraphJin serv auth handles with your own application. Works with routers like chi and http mux.
// For detailed documentation visit https://graphjin.com
//
// Example usage:
/*
	package main

	import (
		"net/http"
		"path/filepath"
		"github.com/go-chi/chi"
		"github.com/dosco/graphjin/serv/v3"
		"github.com/dosco/graphjin/auth/v3"
	)

	func main() {
		conf, err := serv.ReadInConfig(filepath.Join("./config", serv.GetConfigName()))
		if err != nil {
			panic(err)
		}

		useAuth, err := auth.NewAuth(conf.Auth, log, auth.Options{AuthFailBlock: true})
		if err != nil {
			panic(err)
		}

		r := chi.NewRouter()
		r.Use(useAuth)
		r.Get("/user", userInfo)

		http.ListenAndServe(":8080", r)
	}
*/
package auth

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strconv"

	"github.com/dosco/graphjin/core/v3"
	"github.com/gorilla/websocket"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"

	"github.com/dosco/graphjin/auth/v3/provider"
)

type JWTConfig = provider.JWTConfig

// Auth struct contains authentication related config values used by the GraphJin service
type Auth struct {
	// Enable development mode used to set credentials in the header and vars for testing
	Development bool `jsonschema:"title=Development Mode,default=false"`

	// Name is a friendly name for this auth config
	Name string

	// Type can be one of jwt or header
	Type string `jsonschema:"title=Type,enum=jwt,enum=header"`

	// The name of the cookie that holds the authentication token
	Cookie string `jsonschema:"title=Cookie Name"`

	// In certain cases like Magiclink the jwt cookie is generated by us this
	// set the secure parameter of this cookie
	// CookieHTTPS bool `mapstructure:"cookie_https"`

	// In certain cases like Magiclink the jwt cookie is generated by us this
	// set the expiry parameter of this cookie (ex. "20m", "2h")
	// CookieExpiry string `mapstructure:"cookie_expiry"`


	// JWT authentication
	JWT JWTConfig

	// Header authentication
	Header struct {
		// Name of the HTTP header
		Name string

		// Value if set must match expected value (optional)
		Value string

		// Exists if set to true then the header must exist
		// this is an alternative to using value
		Exists bool
	}

	// Magic.link authentication
	// MagicLink struct {
	// 	Secret string
	// }
}

type HandlerFunc func(w http.ResponseWriter, r *http.Request) (context.Context, error)

type Options struct {
	// Return a HTTP '401 Unauthoized' when auth fails
	AuthFailBlock bool
}

// NewAuthHandlerFunc returns a HandlerFunc based on the provided config.
// Usually you don't need to use this function, because is called by NewAuth if
// no HandlerFunc is provided.
func NewAuthHandlerFunc(ac Auth) (HandlerFunc, error) {
	var h HandlerFunc
	var err error

	switch ac.Development {
	case true:
		h, err = SimpleHandler(ac)

	default:
		switch ac.Type {
		case "jwt":
			h, err = JwtHandler(ac)

		case "header":
			h, err = HeaderHandler(ac)

		// case "magiclink":
		// 	h, err = MagicLinkHandler(ac, next)

		case "", "none":
			h, err = NoAuth()

		default:
			return nil, fmt.Errorf("auth: unknown auth type: %s", ac.Type)
		}

		if err != nil {
			return nil, fmt.Errorf("%s: %s", ac.Type, err.Error())
		}
	}
	return h, err
}

// NoAuth returns a handler that does not perform any authentication.
func NoAuth() (HandlerFunc, error) {
	return func(w http.ResponseWriter, r *http.Request) (context.Context, error) {
		return r.Context(), nil
	}, nil
}

// NewAuth returns a new auth handler. It will create a HandlerFunc based on the
// provided config.
//
// Optionally an existing HandlerFunc can be provided. This is required to
// support auth in WS subscriptions.
func NewAuth(ac Auth, log *zap.Logger, opt Options, hFn ...HandlerFunc) (
	func(next http.Handler) http.Handler, error,
) {
	var err error
	var h HandlerFunc
	var wsAuthSupported bool

	if len(hFn) != 0 && hFn[0] != nil {
		h = hFn[0]
		wsAuthSupported = true
	} else {
		h, err = NewAuthHandlerFunc(ac)
		if err != nil {
			return nil, err
		}
	}

	return func(next http.Handler) http.Handler {
		ah := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if wsAuthSupported && websocket.IsWebSocketUpgrade(r) {
				next.ServeHTTP(w, r)
				return
			}
			c, err := h(w, r)
			if err != nil && log != nil {
				log.Error("Auth", []zapcore.Field{zap.String("type", ac.Type), zap.Error(err)}...)
			}

			if err == Err401 {
				http.Error(w, "401 unauthorized", http.StatusUnauthorized)
				return
			}

			if opt.AuthFailBlock && !IsAuth(c) {
				http.Error(w, "401 unauthorized", http.StatusUnauthorized)
				return
			}

			if c != nil {
				next.ServeHTTP(w, r.WithContext(c))
			} else {
				next.ServeHTTP(w, r)
			}
		})

		return ah
	}, nil
}

// SimpleHandler is a simple auth handler that sets the user ID, provider and role
func SimpleHandler(ac Auth) (HandlerFunc, error) {
	return func(_ http.ResponseWriter, r *http.Request) (context.Context, error) {
		c := r.Context()

		userIDProvider := r.Header.Get("X-User-ID-Provider")
		if userIDProvider != "" {
			c = context.WithValue(c, core.UserIDProviderKey, userIDProvider)
		}

		userID := r.Header.Get("X-User-ID")
		if userID != "" {
			c = context.WithValue(c, core.UserIDKey, userID)
		}

		userRole := r.Header.Get("X-User-Role")
		if userRole != "" {
			c = context.WithValue(c, core.UserRoleKey, userRole)
		}

		return c, nil
	}, nil
}

var Err401 = errors.New("401 unauthorized")

// HeaderHandler is a middleware that checks for a header value
func HeaderHandler(ac Auth) (HandlerFunc, error) {
	hdr := ac.Header

	if hdr.Name == "" {
		return nil, fmt.Errorf("auth '%s': no header.name defined", ac.Name)
	}

	if !hdr.Exists && hdr.Value == "" {
		return nil, fmt.Errorf("auth '%s': no header.value defined", ac.Name)
	}

	return func(_ http.ResponseWriter, r *http.Request) (context.Context, error) {
		var fo1 bool
		value := r.Header.Get(hdr.Name)

		switch {
		case hdr.Exists:
			fo1 = (value == "")

		default:
			fo1 = (value != hdr.Value)
		}

		if fo1 {
			return nil, Err401
		}
		return nil, nil
	}, nil
}

// IsAuth returns true if the context contains a user ID
func IsAuth(c context.Context) bool {
	return c != nil && c.Value(core.UserIDKey) != nil
}

// UserID returns the user ID from the context
func UserID(c context.Context) interface{} {
	return c.Value(core.UserIDKey)
}

// UserIDInt returns the user ID from the context as an int
func UserIDInt(c context.Context) int {
	v, ok := UserID(c).(string)
	if !ok {
		return -1
	}
	if v, err := strconv.Atoi(v); err == nil {
		return v
	}
	return -1
}
